/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

/**
 * Test Definition
 *
 * Most tests can use an array named TESTS that will perform most if not all of
 * the necessary checks. Each element in the array must be an object with the
 * following possible properties. Additional properties besides the ones listed
 * below can be added as needed.
 *
 * overrideCallback (optional)
 *   The function to call for the next test. This is typically called when the
 *   wizard page changes but can also be called for other events by the previous
 *   test. If this property isn't defined then the defailtCallback function will
 *   be called. If this property is defined then all other properties are
 *   optional.
 *
 * pageid (required unless overrideCallback is specified)
 *   The expected pageid for the wizard. This property is required unless the
 *   overrideCallback property is defined.
 *
 * extraStartFunction (optional)
 *   The function to call at the beginning of the defaultCallback function. If
 *   the function returns true the defaultCallback function will return early
 *   which allows waiting for a specific condition to be evaluated in the
 *   function specified in the extraStartFunction property before continuing
 *   with the test.
 *
 * extraCheckFunction (optional)
 *   The function to call to perform extra checks in the defaultCallback
 *   function.
 *
 * extraDelayedCheckFunction (optional)
 *   The function to call to perform extra checks in the delayedDefaultCallback
 *   function.
 *
 * buttonStates (optional)
 *   A javascript object representing the expected hidden and disabled attribute
 *   values for the buttons of the current wizard page. The values are checked
 *   in the delayedDefaultCallback function. For information about the structure
 *   of this object refer to the getExpectedButtonStates and checkButtonStates
 *   functions.
 *
 * buttonClick (optional)
 *   The current wizard page button to click at the end of the
 *   delayedDefaultCallback function. If the buttonClick property is defined
 *   then the extraDelayedFinishFunction property can't be specified due to race
 *   conditions in some of the tests and if both of them are specified the test
 *   will intentionally throw.
 *
 * extraDelayedFinishFunction (optional)
 *   The function to call at the end of the delayedDefaultCallback function.
 *
 * ranTest (should not be specified)
 *   When delayedDefaultCallback is called a property named ranTest is added to
 *   the current test it is possible to verify that each test in the TESTS
 *   array has ran.
 *
 * prefHasUserValue (optional)
 *   For comparing the expected value defined by this property with the return
 *   value of prefHasUserValue using gPrefToCheck for the preference name in the
 *   checkPrefHasUserValue function.
 *
 * expectedRadioGroupSelectedIndex (optional)
 *   For comparing the expected selectedIndex attribute value of the wizard's
 *   license page radiogroup selectedIndex attribute in the
 *   checkRadioGroupSelectedIndex function.
 *
 * expectedRemoteContentState (optional)
 *   For comparing the expected remotecontent state attribute value of the
 *   wizard's billboard and license pages in the checkRemoteContentState and
 *   waitForRemoteContentLoaded functions.
 */

// The tests have to use the pageid instead of the pageIndex due to the
// app update wizard's access method being random.
const PAGEID_DUMMY            = "dummy";                 // Done
const PAGEID_CHECKING         = "checking";              // Done
const PAGEID_PLUGIN_UPDATES   = "pluginupdatesfound";
const PAGEID_NO_UPDATES_FOUND = "noupdatesfound";        // Done
const PAGEID_MANUAL_UPDATE    = "manualUpdate"; // Tested on license load failure
const PAGEID_INCOMPAT_CHECK   = "incompatibleCheck";
const PAGEID_FOUND_BASIC      = "updatesfoundbasic";     // Done
const PAGEID_FOUND_BILLBOARD  = "updatesfoundbillboard"; // Done
const PAGEID_LICENSE          = "license";               // Done
const PAGEID_INCOMPAT_LIST    = "incompatibleList";
const PAGEID_DOWNLOADING      = "downloading";           // Done
const PAGEID_ERRORS           = "errors";                // Done
const PAGEID_ERROR_PATCHING   = "errorpatching";         // Done
const PAGEID_FINISHED         = "finished";              // Done
const PAGEID_FINISHED_BKGRD   = "finishedBackground";    // Done
const PAGEID_INSTALLED        = "installed";             // Done

const UPDATE_WINDOW_NAME = "Update:Wizard";

const URL_HOST   = "http://example.com/";
const URL_PATH   = "chrome/toolkit/mozapps/update/test/chrome";
const URL_UPDATE = URL_HOST + URL_PATH + "/update.sjs";

const URI_UPDATE_PROMPT_DIALOG  = "chrome://mozapps/content/update/updates.xul";

const CRC_ERROR = 4;

const DEBUG = false;

const TEST_TIMEOUT = 30000; // 30 seconds
var gTimeoutTimer;

// The following vars are for restoring previous preference values (if present)
// when the test finishes.
var gAppUpdateChannel; // app.update.channel (default prefbranch)
var gAppUpdateEnabled; // app.update.enabled
var gAppUpdateURL;     // app.update.url.override

var gTestCounter = -1;
var gWin;
var gDocElem;
var gPrefToCheck;

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is mozilla.org code.
 *
 * The Initial Developer of the Original Code is
 * the Mozilla Foundation.
 * Portions created by the Initial Developer are Copyright (C) 2010
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Robert Strong <robert.bugzilla@gmail.com> (Original Author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

/* Shared code for xpcshell and mochitests-chrome */

// const Cc, Ci, and Cr are defined in netwerk/test/httpserver/httpd.js so we
// need to define unique ones.
const AUS_Cc = Components.classes;
const AUS_Ci = Components.interfaces;
const AUS_Cr = Components.results;
const AUS_Cu = Components.utils;

const PREF_APP_UPDATE_AUTO              = "app.update.auto";
const PREF_APP_UPDATE_CHANNEL           = "app.update.channel";
const PREF_APP_UPDATE_ENABLED           = "app.update.enabled";
const PREF_APP_UPDATE_IDLETIME          = "app.update.idletime";
const PREF_APP_UPDATE_LOG               = "app.update.log";
const PREF_APP_UPDATE_NEVER_BRANCH      = "app.update.never.";
const PREF_APP_UPDATE_PROMPTWAITTIME    = "app.update.promptWaitTime";
const PREF_APP_UPDATE_SHOW_INSTALLED_UI = "app.update.showInstalledUI";
const PREF_APP_UPDATE_URL               = "app.update.url";
const PREF_APP_UPDATE_URL_DETAILS       = "app.update.url.details";
const PREF_APP_UPDATE_URL_OVERRIDE      = "app.update.url.override";

const PREF_APP_PARTNER_BRANCH           = "app.partner.";
const PREF_DISTRIBUTION_ID              = "distribution.id";
const PREF_DISTRIBUTION_VERSION         = "distribution.version";

const NS_APP_PROFILE_DIR_STARTUP   = "ProfDS";
const NS_APP_USER_PROFILE_50_DIR   = "ProfD";
const NS_GRE_DIR                   = "GreD";
const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
const XRE_UPDATE_ROOT_DIR          = "UpdRootD";

const STATE_NONE            = "null";
const STATE_DOWNLOADING     = "downloading";
const STATE_PENDING         = "pending";
const STATE_APPLYING        = "applying";
const STATE_SUCCEEDED       = "succeeded";
const STATE_DOWNLOAD_FAILED = "download-failed";
const STATE_FAILED          = "failed";

const FILE_BACKUP_LOG     = "backup-update.log";
const FILE_LAST_LOG       = "last-update.log";
const FILE_UPDATES_DB     = "updates.xml";
const FILE_UPDATE_ACTIVE  = "active-update.xml";
const FILE_UPDATE_ARCHIVE = "update.mar";
const FILE_UPDATE_LOG     = "update.log";
const FILE_UPDATE_STATUS  = "update.status";

const MODE_RDONLY   = 0x01;
const MODE_WRONLY   = 0x02;
const MODE_CREATE   = 0x08;
const MODE_APPEND   = 0x10;
const MODE_TRUNCATE = 0x20;

const PR_RDWR        = 0x04;
const PR_CREATE_FILE = 0x08;
const PR_APPEND      = 0x10;
const PR_TRUNCATE    = 0x20;
const PR_SYNC        = 0x40;
const PR_EXCL        = 0x80;

const PERMS_FILE      = 0644;
const PERMS_DIRECTORY = 0755;

const URI_UPDATES_PROPERTIES = "chrome://mozapps/locale/update/updates.properties";
const gUpdateBundle = AUS_Cc["@mozilla.org/intl/stringbundle;1"].
                      getService(AUS_Ci.nsIStringBundleService).
                      createBundle(URI_UPDATES_PROPERTIES);

var gDirSvc = AUS_Cc["@mozilla.org/file/directory_service;1"].
              getService(AUS_Ci.nsIProperties);

__defineGetter__("gAUS", function() {
  delete this.gAUS;
  return this.gAUS = AUS_Cc["@mozilla.org/updates/update-service;1"].
                     getService(AUS_Ci.nsIApplicationUpdateService).
                     QueryInterface(AUS_Ci.nsIApplicationUpdateService2).
                     QueryInterface(AUS_Ci.nsITimerCallback).
                     QueryInterface(AUS_Ci.nsIObserver);
});

__defineGetter__("gUpdateManager", function() {
  delete this.gUpdateManager;
  return this.gUpdateManager = AUS_Cc["@mozilla.org/updates/update-manager;1"].
                               getService(AUS_Ci.nsIUpdateManager);
});

__defineGetter__("gUpdateChecker", function() {
  delete this.gUpdateChecker;
  return this.gUpdateChecker = AUS_Cc["@mozilla.org/updates/update-checker;1"].
                               createInstance(AUS_Ci.nsIUpdateChecker);
});

__defineGetter__("gUP", function() {
  delete this.gUP;
  return this.gUP = AUS_Cc["@mozilla.org/updates/update-prompt;1"].
                    createInstance(AUS_Ci.nsIUpdatePrompt);
});

__defineGetter__("gPref", function() {
  delete this.gPref;
  return this.gPref = AUS_Cc["@mozilla.org/preferences-service;1"].
                      getService(AUS_Ci.nsIPrefBranch2).
                      QueryInterface(AUS_Ci.nsIPrefService);
});

__defineGetter__("gDefaultPrefBranch", function() {
  delete this.gDefaultPrefBranch;
  return this.gDefaultPrefBranch = gPref.getDefaultBranch(null);
});

__defineGetter__("gZipW", function() {
  delete this.gZipW;
  return this.gZipW = AUS_Cc["@mozilla.org/zipwriter;1"].
                      createInstance(AUS_Ci.nsIZipWriter);
});

/* Initializes the update service stub */
function initUpdateServiceStub() {
  AUS_Cc["@mozilla.org/updates/update-service-stub;1"].
  createInstance(AUS_Ci.nsISupports);
}

/* Reloads the update metadata from disk */
function reloadUpdateManagerData() {
  gUpdateManager.QueryInterface(AUS_Ci.nsIObserver).
  observe(null, "um-reload-update-data", "");
}

/**
 * Sets the app.update.channel preference.
 * @param   aChannel
 *          The update channel. If not specified 'test_channel' will be used.
 */
function setUpdateChannel(aChannel) {
  gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL,
                                 aChannel ? aChannel : "test_channel");
}

/**
 * Sets the app.update.url.override preference.
 * @param   aURL
 *          The update url. If not specified 'URL_HOST + "update.xml"' will be
 *          used.
 */
function setUpdateURLOverride(aURL) {
  gPref.setCharPref(PREF_APP_UPDATE_URL_OVERRIDE,
                    (aURL ? aURL : URL_HOST + "update.xml"));
}

/**
 * Constructs a string representing a remote update xml file.
 * @param   aUpdates
 *          The string representing the update elements.
 * @returns The string representing a remote update xml file.
 */
function getRemoteUpdatesXMLString(aUpdates) {
  return "<?xml version=\"1.0\"?>\n" +
         "<updates>\n" +
           aUpdates +
         "</updates>\n";
}

/**
 * Constructs a string representing an update element for a remote update xml
 * file.
 * See getUpdateString
 * @returns The string representing an update element for an update xml file.
 */
function getRemoteUpdateString(aPatches, aType, aName, aVersion,
                               aExtensionVersion, aPlatformVersion, aBuildID,
                               aDetailsURL, aLicenseURL) {
  return  getUpdateString(aType, aName, aVersion, aExtensionVersion,
                          aPlatformVersion, aBuildID, aDetailsURL,
                          aLicenseURL) + ">\n" +
              aPatches + 
         "  </update>\n";
}

/**
 * Constructs a string representing a patch element for a remote update xml
 * file
 * See getPatchString
 * @returns The string representing a patch element for a remote update xml
 *          file.
 */
function getRemotePatchString(aType, aURL, aHashFunction, aHashValue, aSize) {
  return getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) +
         "/>\n";
}

/**
 * Constructs a string representing a local update xml file.
 * @param   aUpdates
 *          The string representing the update elements.
 * @returns The string representing a local update xml file.
 */
function getLocalUpdatesXMLString(aUpdates) {
  if (!aUpdates || aUpdates == "")
    return "<updates xmlns=\"http://www.mozilla.org/2005/app-update\"/>"
  return ("<updates xmlns=\"http://www.mozilla.org/2005/app-update\">" +
           aUpdates +
         "</updates>").replace(/>\s+\n*</g,'><');
}

/**
 * Constructs a string representing an update element for a local update xml
 * file.
 * See getUpdateString
 * @param   aServiceURL
 *          The update's xml url.
 *          If null will default to 'http://test_service/'.
 * @param   aIsCompleteUpdate
 *          The string 'true' if this update was a complete update or the string
 *          'false' if this update was a partial update.
 *          If null will default to 'true'.
 * @param   aChannel
 *          The update channel name.
 *          If null will default to 'test_channel'.
 * @param   aForegroundDownload
 *          The string 'true' if this update was manually downloaded or the
 *          string 'false' if this update was automatically downloaded.
 *          If null will default to 'true'.
 * @returns The string representing an update element for an update xml file.
 */
function getLocalUpdateString(aPatches, aType, aName, aVersion,
                              aExtensionVersion, aPlatformVersion, aBuildID,
                              aDetailsURL, aLicenseURL, aServiceURL,
                              aInstallDate, aStatusText, aIsCompleteUpdate,
                              aChannel, aForegroundDownload) {
  var serviceURL = aServiceURL ? aServiceURL : "http://test_service/";
  var installDate = aInstallDate ? aInstallDate : "1238441400314";
  var statusText = aStatusText ? aStatusText : "Install Pending";
  var isCompleteUpdate = typeof(aIsCompleteUpdate) == "string" ? aIsCompleteUpdate : "true";
  var channel = aChannel ? aChannel : "test_channel";
  var foregroundDownload =
    typeof(aForegroundDownload) == "string" ? aForegroundDownload : "true";
  return getUpdateString(aType, aName, aVersion, aExtensionVersion,
                         aPlatformVersion, aBuildID, aDetailsURL, aLicenseURL) +
                   " " +
                   "serviceURL=\"" + serviceURL + "\" " +
                   "installDate=\"" + installDate + "\" " +
                   "statusText=\"" + statusText + "\" " +
                   "isCompleteUpdate=\"" + isCompleteUpdate + "\" " +
                   "channel=\"" + channel + "\" " +
                   "foregroundDownload=\"" + foregroundDownload + "\">"  +
              aPatches + 
         "  </update>";
}

/**
 * Constructs a string representing a patch element for a local update xml file.
 * See getPatchString
 * @param   aSelected
 *          Whether this patch is selected represented or not. The string 'true'
 *          denotes selected and the string 'false' denotes not selected.
 *          If null will default to the string 'true'.
 * @param   aState
 *          The patch's state.
 *          If null will default to STATE_SUCCEEDED (e.g. 'succeeded').
 * @returns The string representing a patch element for a local update xml file.
 */
function getLocalPatchString(aType, aURL, aHashFunction, aHashValue, aSize,
                             aSelected, aState) {
  var selected = typeof(aSelected) == "string" ? aSelected : "true";
  var state = aState ? aState : STATE_SUCCEEDED;
  return getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) + " " +
         "selected=\"" + selected + "\" " +
         "state=\"" + state + "\"/>\n";
}

/**
 * Constructs a string representing an update element for a remote update xml
 * file.
 * @param   aType
 *          The update's type which should be major or minor.
 *          If null will default to 'major'.
 * @param   aName
 *          The update's name.
 *          If null will default to 'App Update Test'.
 * @param   aVersion
 *          The update's display version.
 *          If null will default to 'version 99.0'.
 * @param   aExtensionVersion
 *          The update's application version.
 *          If null will default to '99.0'.
 * @param   aPlatformVersion
 *          The update's platform version.
 *          If null will default to '99.0'.
 * @param   aBuildID
 *          The update's build id.
 *          If null will default to '20080811053724'.
 * @param   aDetailsURL
 *          The update's details url.
 *          If null will default to 'http://test_details/' due to due to
 *          bug 470244.
 * @param   aLicenseURL
 *          The update's license url.
 *          If null will not be added.
 * @returns The string representing an update element for an update xml file.
 */
function getUpdateString(aType, aName, aVersion, aExtensionVersion,
                         aPlatformVersion, aBuildID, aDetailsURL, aLicenseURL) {
  var type = aType ? aType : "major";
  var name = aName ? aName : "App Update Test";
  var version = "version=\"" + (aVersion ? aVersion
                                         : "version 99.0") + "\" ";
  var extensionVersion = "extensionVersion=\"" +
                         (aExtensionVersion ? aExtensionVersion
                                            : "99.0") + "\" ";
  var platformVersion = "";
  if (aPlatformVersion) {
    platformVersion = "platformVersion=\"" + (aPlatformVersion ? aPlatformVersion : "99.0") + "\" ";
  }
  var buildID = aBuildID ? aBuildID : "20080811053724";
  // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244
//   var detailsURL = aDetailsURL ? "detailsURL=\"" + aDetailsURL + "\" " : "";
  var detailsURL = "detailsURL=\"" + (aDetailsURL ? aDetailsURL : "http://test_details/") + "\" ";
  var licenseURL = aLicenseURL ? "licenseURL=\"" + aLicenseURL + "\" " : "";
  return "  <update type=\"" + type + "\" " +
                   "name=\"" + name + "\" " +
                   version +
                   extensionVersion +
                   platformVersion +
                   detailsURL +
                   licenseURL +
                   "buildID=\"" + buildID + "\"";
}

/**
 * Constructs a string representing a patch element for an update xml file.
 * @param   aType
 *          The patch's type which should be complete or partial.
 *          If null will default to 'complete'.
 * @param   aURL
 *          The patch's url to the mar file.
 *          If null will default to 'http://localhost:4444/data/empty.mar'.
 * @param   aHashFunction
 *          The patch's hash function used to verify the mar file.
 *          If null will default to 'MD5'.
 * @param   aHashValue
 *          The patch's hash value used to verify the mar file.
 *          If null will default to '6232cd43a1c77e30191c53a329a3f99d'
 *          which is the MD5 hash value for the empty.mar.
 * @param   aSize
 *          The patch's file size for the mar file.
 *          If null will default to '775' which is the file size for the
 *          empty.mar.
 * @returns The string representing a patch element for an update xml file.
 */
function getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) {
  var type = aType ? aType : "complete";
  var url = aURL ? aURL : URL_HOST + URL_PATH + "/empty.mar";
  var hashFunction = aHashFunction ? aHashFunction : "MD5";
  var hashValue = aHashValue ? aHashValue : "6232cd43a1c77e30191c53a329a3f99d";
  var size = aSize ? aSize : "775";
  return "    <patch type=\"" + type + "\" " +
                     "URL=\"" + url + "\" " +
                     "hashFunction=\"" + hashFunction + "\" " +
                     "hashValue=\"" + hashValue + "\" " +
                     "size=\"" + size + "\"";
}

/**
 * Writes the updates specified to either the active-update.xml or the
 * updates.xml.
 * @param   updates
 *          The updates represented as a string to write to the XML file.
 * @param   isActiveUpdate
 *          If true this will write to the active-update.xml otherwise it will
 *          write to the updates.xml file.
 */
function writeUpdatesToXMLFile(aContent, aIsActiveUpdate) {
  var file = getCurrentProcessDir();
  file.append(aIsActiveUpdate ? FILE_UPDATE_ACTIVE : FILE_UPDATES_DB);
  writeFile(file, aContent);
}

/**
 * Writes the current update operation/state to a file in the patch
 * directory, indicating to the patching system that operations need
 * to be performed.
 * @param   aStatus
 *          The status value to write.
 */
function writeStatusFile(aStatus) {
  var file = getUpdatesDir();
  file.append("0");
  file.append(FILE_UPDATE_STATUS);
  aStatus += "\n";
  writeFile(file, aStatus);
}

/**
 * Gets the updates directory.
 * @returns The updates directory.
 */
function getUpdatesDir() {
  var dir = getCurrentProcessDir();
  dir.append("updates");
  return dir;
}

/**
 * Writes text to a file. This will replace existing text if the file exists
 * and create the file if it doesn't exist.
 * @param   aFile
 *          The file to write to. Will be created if it doesn't exist.
 * @param   aText
 *          The text to write to the file. If there is existing text it will be
 *          replaced.
 */
function writeFile(aFile, aText) {
  var fos = AUS_Cc["@mozilla.org/network/file-output-stream;1"].
            createInstance(AUS_Ci.nsIFileOutputStream);
  if (!aFile.exists())
    aFile.create(AUS_Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
  fos.init(aFile, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, PERMS_FILE, 0);
  fos.write(aText, aText.length);
  fos.close();
}

/**
 * Reads text from a file and returns the string.
 * @param   aFile
 *          The file to read from.
 * @returns The string of text read from the file.
 */
function readFile(aFile) {
  var fis = AUS_Cc["@mozilla.org/network/file-input-stream;1"].
            createInstance(AUS_Ci.nsIFileInputStream);
  if (!aFile.exists())
    return null;
  fis.init(aFile, MODE_RDONLY, PERMS_FILE, 0);
  var sis = AUS_Cc["@mozilla.org/scriptableinputstream;1"].
            createInstance(AUS_Ci.nsIScriptableInputStream);
  sis.init(fis);
  var text = sis.read(sis.available());
  sis.close();
  return text;
}

/**
 * Reads the binary contents of a file and returns is as a string.
 * @param   aFile
 *          The file to read from.
 * @returns The contents of the file as a string.
 */
function readFileBytes(aFile) {
  var fis = AUS_Cc["@mozilla.org/network/file-input-stream;1"].
            createInstance(AUS_Ci.nsIFileInputStream);
  fis.init(aFile, -1, -1, false);
   var bis = AUS_Cc["@mozilla.org/binaryinputstream;1"].
             createInstance(AUS_Ci.nsIBinaryInputStream);
   bis.setInputStream(fis);
   var data = [];
   var count = fis.available();
   while (count > 0) {
     var bytes = bis.readByteArray(Math.min(65535, count));
     data.push(String.fromCharCode.apply(null, bytes));
     count -= bytes.length;
     if (bytes.length == 0)
       do_throw("Nothing read from input stream!");
   }
  data.join('');
  fis.close();
  return data.toString();
}

/* Returns human readable status text from the updates.properties bundle */
function getStatusText(aErrCode) {
  return getString("check_error-" + aErrCode);
}

/* Returns a string from the updates.properties bundle */
function getString(aName) {
  try {
    return gUpdateBundle.GetStringFromName(aName);
  }
  catch (e) {
  }
  return null;
}

/**
 * Removes the updates.xml file, active-update.xml file, and all files and
 * sub-directories in the updates directory except for the "0" sub-directory.
 * This prevents some tests from failing due to files being left behind when the
 * tests are interrupted.
 */
function removeUpdateDirsAndFiles() {
  var appDir = getCurrentProcessDir();
  var file = appDir.clone();
  file.append(FILE_UPDATE_ACTIVE);
  try {
    if (file.exists())
      file.remove(false);
  }
  catch (e) {
    dump("Unable to remove file\npath: " + file.path +
         "\nException: " + e + "\n");
  }

  file = appDir.clone();
  file.append(FILE_UPDATES_DB);
  try {
    if (file.exists())
      file.remove(false);
  }
  catch (e) {
    dump("Unable to remove file\npath: " + file.path +
         "\nException: " + e + "\n");
  }

  // This fails sporadically on Mac OS X so wrap it in a try catch
  var updatesDir = appDir.clone();
  updatesDir.append("updates");
  try {
    cleanUpdatesDir(updatesDir);
  }
  catch (e) {
    dump("Unable to remove files / directories from directory\npath: " +
         updatesDir.path + "\nException: " + e + "\n");
  }
}

/**
 * Removes all files and sub-directories in the updates directory except for
 * the "0" sub-directory.
 * @param   dir
 *          A nsIFile for the directory to be deleted
 */
function cleanUpdatesDir(aDir) {
  if (!aDir.exists())
    return;

  var dirEntries = aDir.directoryEntries;
  while (dirEntries.hasMoreElements()) {
    var entry = dirEntries.getNext().QueryInterface(AUS_Ci.nsIFile);

    if (entry.isDirectory()) {
      if (entry.leafName == "0" && entry.parent.leafName == "updates") {
        cleanUpdatesDir(entry);
        entry.permissions = PERMS_DIRECTORY;
      }
      else {
        try {
          entry.remove(true);
          return;
        }
        catch (e) {
        }
        cleanUpdatesDir(entry);
        entry.permissions = PERMS_DIRECTORY;
        entry.remove(true);
      }
    }
    else {
      entry.permissions = PERMS_FILE;
      entry.remove(false);
    }
  }
}

/**
 * Deletes a directory and its children. First it tries nsIFile::Remove(true).
 * If that fails it will fall back to recursing, setting the appropriate
 * permissions, and deleting the current entry.
 * @param   dir
 *          A nsIFile for the directory to be deleted
 */
function removeDirRecursive(aDir) {
  if (!aDir.exists())
    return;
  try {
    aDir.remove(true);
    return;
  }
  catch (e) {
  }

  var dirEntries = aDir.directoryEntries;
  while (dirEntries.hasMoreElements()) {
    var entry = dirEntries.getNext().QueryInterface(AUS_Ci.nsIFile);

    if (entry.isDirectory()) {
      removeDirRecursive(entry);
    }
    else {
      entry.permissions = PERMS_FILE;
      entry.remove(false);
    }
  }
  aDir.permissions = PERMS_DIRECTORY;
  aDir.remove(true);
}

/**
 * Returns the directory for the currently running process. This is used to
 * clean up after the tests and to locate the active-update.xml and updates.xml
 * files.
 */
function getCurrentProcessDir() {
  return gDirSvc.get(NS_XPCOM_CURRENT_PROCESS_DIR, AUS_Ci.nsIFile);
}

/**
 * Returns the Gecko Runtime Engine directory. This is used to locate the the
 * updater binary (Windows and Linux) or updater package (Mac OS X). For
 * XULRunner applications this is different than the currently running process
 * directory.
 */
function getGREDir() {
  return gDirSvc.get(NS_GRE_DIR, AUS_Ci.nsIFile);
}
//@line 123 "/builds/slave/macosx_build/build/toolkit/mozapps/update/test/chrome/utils.js"

function debugDump(msg) {
  if (DEBUG) {
    dump("*** " + msg + "\n");
  }
}

__defineGetter__("gWW", function() {
  delete this.gWW;
  return this.gWW = AUS_Cc["@mozilla.org/embedcomp/window-watcher;1"].
                      getService(AUS_Ci.nsIWindowWatcher);
});

__defineGetter__("gApp", function() {
  delete this.gApp;
  return this.gApp = AUS_Cc["@mozilla.org/xre/app-info;1"].
                     getService(AUS_Ci.nsIXULAppInfo).
                     QueryInterface(AUS_Ci.nsIXULRuntime);
});

/**
 * The current test in TESTS array.
 */
__defineGetter__("gTest", function() {
  return TESTS[gTestCounter];
});

/**
 * The current test's callback. This will either return the callback defined in
 * the test's overrideCallback property or defaultCallback if the
 * overrideCallback property is undefined.
 */
__defineGetter__("gCallback", function() {
  return gTest.overrideCallback ? gTest.overrideCallback
                                : defaultCallback;
});

/**
 * The remotecontent element for the current page if one exists or null if a
 * remotecontent element doesn't exist.
 */
__defineGetter__("gRemoteContent", function() {
  switch (gTest.pageid) {
    case PAGEID_FOUND_BILLBOARD:
      return gWin.document.getElementById("updateMoreInfoContent");
    case PAGEID_LICENSE:
      return gWin.document.getElementById("licenseContent");
  }
  return null;
});

/**
 * The state for the remotecontent element if one exists or null if a
 * remotecontent element doesn't exist.
 */
__defineGetter__("gRemoteContentState", function() {
  if (gRemoteContent) {
    return gRemoteContent.getAttribute("state");
  }
  return null;
});

/**
 * The radiogroup for the license page.
 */
__defineGetter__("gAcceptDeclineLicense", function() {
  return gWin.document.getElementById("acceptDeclineLicense");
});

/**
 * The listbox for the incompatibleList page.
 */
__defineGetter__("gIncompatibleListbox", function() {
  return gWin.document.getElementById("incompatibleListbox");
});

/**
 * Default test run function that can be used by most tests.
 */
function runTestDefault() {
  debugDump("Entering runTestDefault");

  SimpleTest.waitForExplicitFinish();

  gWW.registerNotification(gWindowObserver);

  setupPrefs();
  removeUpdateDirsAndFiles();
  reloadUpdateManagerData();
  runTest();
}

/**
 * Default test finish function that can be used by most tests.
 */
function finishTestDefault() {
  debugDump("Entering finishTestDefault");

  gDocElem.removeEventListener("pageshow", onPageShowDefault, false);

  if (gTimeoutTimer) {
    gTimeoutTimer.cancel();
    gTimeoutTimer = null;
  }

  verifyTestsRan();

  gWW.unregisterNotification(gWindowObserver);

  resetPrefs();
  removeUpdateDirsAndFiles();
  reloadUpdateManagerData();
  SimpleTest.finish();
}

/**
 * nsITimerCallback for the timeout timer to cleanly finish a test if the Update
 * Window doesn't close for a test. This allows the next test to run properly if
 * a previous test fails.
 *
 * @param  aTimer
 *         The nsITimer that fired.
 */
function finishTestTimeout(aTimer) {
  gTimeoutTimer = null;
  ok(false, "Test timed out. Maximum time allowed is " + (TEST_TIMEOUT / 1000) +
     " seconds");
  gWin.close();
}

/**
 * Default callback for the wizard's documentElement pageshow listener. This
 * will return early for event's where the originalTarget's nodeName is not
 * wizardpage.
 */
function onPageShowDefault(aEvent) {
  // Return early if the event's original target isn't for a wizardpage element.
  // This check is necessary due to the remotecontent element firing pageshow.
  if (aEvent.originalTarget.nodeName != "wizardpage") {
    debugDump("onPageShowDefault - only handles events with an " +
              "originalTarget nodeName of |wizardpage|. " +
              "aEvent.originalTarget.nodeName = " +
              aEvent.originalTarget.nodeName + "... returning early");
    return;
  }

  gTestCounter++;
  gCallback(aEvent);
}

/**
 * Default callback that can be used by most tests.
 */
function defaultCallback(aEvent) {
  debugDump("Entering defaultCallback - TESTS[" + gTestCounter + "], " +
            "pageid: " + gTest.pageid + ", currentPage.pageid: " +
            gDocElem.currentPage.pageid + ", " +
            "aEvent.originalTarget.nodeName: " + aEvent.originalTarget.nodeName);

  if (gTest && gTest.extraStartFunction) {
    debugDump("defaultCallback - calling extraStartFunction " +
              gTest.extraStartFunction.name);
    if (gTest.extraStartFunction(aEvent)) {
      debugDump("defaultCallback - extraStartFunction early return");
      return;
    }
  }

  is(gDocElem.currentPage.pageid, gTest.pageid,
     "Checking currentPage.pageid equals " + gTest.pageid + " in pageshow");

  // Perform extra checks if specified by the test
  if (gTest.extraCheckFunction) {
    debugDump("delayedCallback - calling extraCheckFunction " +
              gTest.extraCheckFunction.name);
    gTest.extraCheckFunction();
  }

  // The wizard page buttons' disabled and hidden attributes are set after the
  // pageshow event so use executeSoon to allow them to be set so their disabled
  // and hidden attribute values can be checked.
  SimpleTest.executeSoon(delayedDefaultCallback);
}

/**
 * Delayed default callback called using executeSoon in defaultCallback which
 * allows the wizard page buttons' disabled and hidden attributes to be set
 * before checking their values.
 */
function delayedDefaultCallback() {
  debugDump("Entering delayedDefaultCallback - TESTS[" + gTestCounter + "], " +
            "pageid: " + gTest.pageid + ", currentPage.pageid: " +
            gDocElem.currentPage.pageid);

  // Verify the pageid hasn't changed after executeSoon was called.
  is(gDocElem.currentPage.pageid, gTest.pageid,
     "Checking currentPage.pageid equals " + gTest.pageid + " after " +
     "executeSoon");

  checkButtonStates();

  // Perform delayed extra checks if specified by the test
  if (gTest.extraDelayedCheckFunction) {
    debugDump("delayedDefaultCallback - calling extraDelayedCheckFunction " +
              gTest.extraDelayedCheckFunction.name);
    gTest.extraDelayedCheckFunction();
  }

  // Used to verify that this test has been performed
  gTest.ranTest = true;

  if (gTest.buttonClick) {
    debugDump("delayedDefaultCallback - clicking " + gTest.buttonClick +
              " button");
    if(gTest.extraDelayedFinishFunction) {
      throw("Tests cannot have a buttonClick and an extraDelayedFinishFunction property");
    }
    gDocElem.getButton(gTest.buttonClick).click();
  }
  else if (gTest.extraDelayedFinishFunction) {
    debugDump("delayedDefaultCallback - calling extraDelayedFinishFunction " +
              gTest.extraDelayedFinishFunction.name);
    gTest.extraDelayedFinishFunction();
  }
}

/**
 * Checks the wizard page buttons' disabled and hidden attributes values are
 * correct. If an expected button id is not specified then the expected disabled
 * and hidden attribute value is true.
 */
function checkButtonStates() {
  debugDump("Entering checkButtonStates - TESTS[" + gTestCounter + "], " +
            "pageid: " + gTest.pageid + ", currentPage.pageid: " +
            gDocElem.currentPage.pageid);

  const buttonNames = ["extra1", "extra2", "back", "next", "finish", "cancel"];
  let buttonStates = getExpectedButtonStates();
  buttonNames.forEach(function(aButtonName) {
    let button = gDocElem.getButton(aButtonName);
    let hasHidden = aButtonName in buttonStates &&
                    "hidden" in buttonStates[aButtonName];
    let hidden = hasHidden ? buttonStates[aButtonName].hidden : true;
    let hasDisabled = aButtonName in buttonStates &&
                      "disabled" in buttonStates[aButtonName];
    let disabled = hasDisabled ? buttonStates[aButtonName].disabled : true;
    is(button.hidden, hidden, "Checking " + aButtonName + " button " +
       "hidden attribute value equals " + (hidden ? "true" : "false"));
    if (button.hidden != hidden)
      debugDump("Checking " + aButtonName + " button hidden attribute " +
                "value equals " + (hidden ? "true" : "false"));
    is(button.disabled, disabled, "Checking " + aButtonName + " button " +
       "disabled attribute value equals " + (disabled ? "true" : "false"));
    if (button.disabled != disabled)
      debugDump("Checking " + aButtonName + " button disabled attribute " +
                "value equals " + (disabled ? "true" : "false"));
  });
}

/**
 * Returns the expected disabled and hidden attribute values for the buttons of
 * the current wizard page.
 */
function getExpectedButtonStates() {
  // Allow individual tests to override the expected button states.
  if (gTest.buttonStates) {
    return gTest.buttonStates;
  }

  switch (gTest.pageid) {
    case PAGEID_CHECKING:
    case PAGEID_INCOMPAT_CHECK:
      return { cancel: { disabled: false, hidden: false } };
    case PAGEID_FOUND_BASIC:
      return { extra1: { disabled: false, hidden: false },
               next  : { disabled: false, hidden: false } }
    case PAGEID_FOUND_BILLBOARD:
      return { extra1: { disabled: false, hidden: false },
               extra2: { disabled: false, hidden: false },
               next  : { disabled: false, hidden: false } }
    case PAGEID_LICENSE:
      if (gRemoteContentState != "loaded" ||
          gAcceptDeclineLicense.selectedIndex != 0) {
        return { extra1: { disabled: false, hidden: false },
                 next  : { disabled: true, hidden: false } };
      }
      return { extra1: { disabled: false, hidden: false },
               next  : { disabled: false, hidden: false } };
    case PAGEID_INCOMPAT_LIST:
      return { extra1: { disabled: false, hidden: false },
               next  : { disabled: false, hidden: false } };
    case PAGEID_DOWNLOADING:
      return { extra1: { disabled: false, hidden: false } };
    case PAGEID_NO_UPDATES_FOUND:
    case PAGEID_MANUAL_UPDATE:
    case PAGEID_ERRORS:
    case PAGEID_INSTALLED:
      return { finish: { disabled: false, hidden: false } };
    case PAGEID_ERROR_PATCHING:
      return { next  : { disabled: false, hidden: false } };
    case PAGEID_FINISHED:
    case PAGEID_FINISHED_BKGRD:
      return { extra1: { disabled: false, hidden: false },
               finish: { disabled: false, hidden: false } };
  }
  return null;
}

/**
 * Adds a load event listener to the current remotecontent element.
 */
function addRemoteContentLoadListener() {
  debugDump("Entering addRemoteContentLoadListener - TESTS[" + gTestCounter +
            "], pageid: " + gTest.pageid);

  gRemoteContent.addEventListener("load", remoteContentLoadListener, false);
}

/**
 * The nsIDOMEventListener for a remotecontent load event.
 */
function remoteContentLoadListener(aEvent) {
  // Return early if the event's original target's nodeName isn't remotecontent.
  if (aEvent.originalTarget.nodeName != "remotecontent") {
    debugDump("remoteContentLoadListener - only handles events with an " +
              "originalTarget nodeName of |remotecontent|. " +
              "aEvent.originalTarget.nodeName = " +
              aEvent.originalTarget.nodeName);
    return;
  }

  gTestCounter++;
  gCallback(aEvent);
}

/**
 * Waits until a remotecontent element to finish loading which is determined
 * by the current test's expectedRemoteContentState property and then removes
 * the event listener.
 *
 * Note: tests that use this function should not test the state of the
 *      remotecontent since this will check the expected state.
 *
 * @return false if the remotecontent has loaded and its state is the state
 *         specified in the current test's expectedRemoteContentState
 *         property... otherwise true.
 */
function waitForRemoteContentLoaded(aEvent) {
  // Return early until the remotecontent has loaded with the state that is
  // expected or isn't the event's originalTarget.
  if (gRemoteContentState != gTest.expectedRemoteContentState ||
      !aEvent.originalTarget.isSameNode(gRemoteContent)) {
    debugDump("waitForRemoteContentLoaded - returning early\n" +
              "gRemoteContentState: " + gRemoteContentState + "\n" +
              "expectedRemoteContentState: " +
              gTest.expectedRemoteContentState + "\n" +
              "aEvent.originalTarget.nodeName: " +
              aEvent.originalTarget.nodeName);
    return true;
  }

  gRemoteContent.removeEventListener("load", remoteContentLoadListener, false);
  return false;
}

/**
 * Compares the value of the remotecontent state attribute with the value
 * specified in the test's expectedRemoteContentState property.
 */
function checkRemoteContentState() {
  is(gRemoteContentState, gTest.expectedRemoteContentState, "Checking remote " +
     "content state equals " + gTest.expectedRemoteContentState + " - pageid " +
     gTest.pageid);
}

/**
 * Adds a select event listener to the license radiogroup element and clicks
 * the radio element specified in the current test's radioClick property.
 */
function addRadioGroupSelectListenerAndClick() {
  debugDump("Entering addRadioGroupSelectListenerAndClick - TESTS[" +
            gTestCounter + "], pageid: " + gTest.pageid);

  gAcceptDeclineLicense.addEventListener("select", radioGroupSelectListener,
                                         false);
  gWin.document.getElementById(gTest.radioClick).click();
}

/**
 * The nsIDOMEventListener for the license radiogroup select event.
 */
function radioGroupSelectListener(aEvent) {
  // Return early if the event's original target's nodeName isn't radiogroup.
  if (aEvent.originalTarget.nodeName != "radiogroup") {
    debugDump("remoteContentLoadListener - only handles events with an " +
              "originalTarget nodeName of |radiogroup|. " +
              "aEvent.originalTarget.nodeName = " +
              aEvent.originalTarget.nodeName);
    return;
  }

  gAcceptDeclineLicense.removeEventListener("select", radioGroupSelectListener,
                                            false);
  gTestCounter++;
  gCallback(aEvent);
}

/**
 * Compares the value of the License radiogroup's selectedIndex attribute with
 * the value specified in the test's expectedRadioGroupSelectedIndex property.
 */
function checkRadioGroupSelectedIndex() {
  is(gAcceptDeclineLicense.selectedIndex, gTest.expectedRadioGroupSelectedIndex,
     "Checking license radiogroup selectedIndex equals " +
     gTest.expectedRadioGroupSelectedIndex);
}

/**
 * Compares the return value of prefHasUserValue for the preference specified in
 * gPrefToCheck with the value passed in the aPrefHasValue parameter or the
 * value specified in the current test's prefHasUserValue property if
 * aPrefHasValue is undefined.
 *
 * @param  aPrefHasValue (optional)
 *         The expected value returned from prefHasUserValue for the preference
 *         specified in gPrefToCheck. If aPrefHasValue is undefined the value
 *         of the current test's prefHasUserValue property will be used.
 */
function checkPrefHasUserValue(aPrefHasValue) {
  let prefHasUserValue = aPrefHasValue === undefined ? gTest.prefHasUserValue
                                                     : aPrefHasValue;
  is(gPref.prefHasUserValue(gPrefToCheck), prefHasUserValue,
     "Checking prefHasUserValue for preference " + gPrefToCheck + " equals " +
     (prefHasUserValue ? "true" : "false"));
}

/**
 * Gets the update version info for the update url parameters to send to
 * update.sjs.
 *
 * @param  aExtensionVersion (optional)
 *         The application version for the update snippet. If not specified the
 *         current application version will be used.
 * @param  aPlatformVersion (optional)
 *         The platform version for the update snippet. If not specified the
 *         current platform version will be used.
 * @return The url parameters for the application and platform version to send
 *         to update.sjs.
 */
function getVersionParams(aExtensionVersion, aPlatformVersion) {
  return "&extensionVersion=" + (aExtensionVersion ? aExtensionVersion
                                                   : gApp.version) +
         "&platformVersion=" + (aPlatformVersion ? aPlatformVersion
                                                 : gApp.platformVersion);
}

/**
 * Verifies that all tests ran.
 */
function verifyTestsRan() {
  debugDump("Entering verifyTestsRan");

  // Return early if there are no tests defined.
  if (!TESTS) {
    return;
  }

  gTestCounter = -1;
  for (let i = 0; i < TESTS.length; ++i) {
    gTestCounter++;
    let test = TESTS[i];
    let msg = "Checking if TESTS[" + i + "] test was performed... " +
              "callback function name = " + gCallback.name + ", " +
              "pageid = " + test.pageid;
    ok(test.ranTest, msg);
  }
}

/**
 * Sets the most common preferences used by tests to values used by the tests
 * and saves some of the preference's original values if present so they can be
 * set back to the original values when each test has finished.
 */
function setupPrefs() {
  if (DEBUG) {
    gPref.setBoolPref(PREF_APP_UPDATE_LOG, true)
  }

  if (gPref.prefHasUserValue(PREF_APP_UPDATE_URL_OVERRIDE)) {
    gAppUpdateURL = gPref.setIntPref(PREF_APP_UPDATE_URL_OVERRIDE);
  }

  gAppUpdateChannel = gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL);
  setUpdateChannel();

  if (gPref.prefHasUserValue(PREF_APP_UPDATE_ENABLED)) {
    gAppUpdateEnabled = gPref.getBoolPref(PREF_APP_UPDATE_ENABLED);
  }

  gPref.setBoolPref(PREF_APP_UPDATE_AUTO, false);
  gPref.setIntPref(PREF_APP_UPDATE_IDLETIME, 0);
  gPref.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 0);
}

/**
 * Resets the most common preferences used by tests to their original values.
 */
function resetPrefs() {
  if (gAppUpdateURL) {
    gPref.setCharPref(PREF_APP_UPDATE_URL_OVERRIDE, gAppUpdateURL);
  }
  else if (gPref.prefHasUserValue(PREF_APP_UPDATE_URL_OVERRIDE)) {
    gPref.clearUserPref(PREF_APP_UPDATE_URL_OVERRIDE);
  }

  if (gAppUpdateChannel) {
    setUpdateChannel(gAppUpdateChannel);
  }

  if (gAppUpdateEnabled) {
    gPref.setBoolPref(PREF_APP_UPDATE_ENABLED, gAppUpdateEnabled);
  }
  else if (gPref.prefHasUserValue(PREF_APP_UPDATE_ENABLED)) {
    gPref.clearUserPref(PREF_APP_UPDATE_ENABLED);
  }

  if (gPref.prefHasUserValue(PREF_APP_UPDATE_IDLETIME)) {
    gPref.clearUserPref(PREF_APP_UPDATE_IDLETIME);
  }

  if (gPref.prefHasUserValue(PREF_APP_UPDATE_PROMPTWAITTIME)) {
    gPref.clearUserPref(PREF_APP_UPDATE_PROMPTWAITTIME);
  }

  if (gPref.prefHasUserValue(PREF_APP_UPDATE_URL_DETAILS)) {
    gPref.clearUserPref(PREF_APP_UPDATE_URL_DETAILS);
  }

  if (gPref.prefHasUserValue(PREF_APP_UPDATE_SHOW_INSTALLED_UI)) {
    gPref.clearUserPref(PREF_APP_UPDATE_SHOW_INSTALLED_UI);
  }

  if (gPref.prefHasUserValue(PREF_APP_UPDATE_LOG)) {
    gPref.clearUserPref(PREF_APP_UPDATE_LOG);
  }

  try {
    gPref.deleteBranch(PREF_APP_UPDATE_NEVER_BRANCH);
  }
  catch(e) {
  }
}

/**
 * Closes the update window if it is open.
 */
function closeUpdateWindow() {
  let updateWindow = getUpdateWindow();
  if (!updateWindow)
    return;

  ok(false, "Found an existing Update Window from a previous test... " +
            "attempting to close it.");
  updateWindow.close();
}

/**
 * Gets the update window.
 *
 * @return The nsIDOMWindowInternal for the Update Window if it is open and null
 *         if it isn't.
 */
function getUpdateWindow() {
  var wm = AUS_Cc["@mozilla.org/appshell/window-mediator;1"].
           getService(AUS_Ci.nsIWindowMediator);
  return wm.getMostRecentWindow(UPDATE_WINDOW_NAME);
}

/**
 * nsIObserver for receiving window open and close notifications.
 */
var gWindowObserver = {
  observe: function WO_observe(aSubject, aTopic, aData) {
    let win = aSubject.QueryInterface(AUS_Ci.nsIDOMEventTarget);

    if (aTopic == "domwindowclosed") {
      if (win.location != URI_UPDATE_PROMPT_DIALOG) {
        debugDump("gWindowObserver:observe - domwindowclosed event for " +
                  "window not being tested - location: " + win.location +
                  "... returning early");
        return;
      }
      // Allow tests the ability to provide their own function (it must be
      // named finishTest) for finishing the test.
      try {
        finishTest();
      }
      catch (e) {
        finishTestDefault();
      }
      return;
    }

    win.addEventListener("load", function onLoad() {
      win.removeEventListener("load", onLoad, false);
      // Ignore windows other than the update UI window.
      if (win.location != URI_UPDATE_PROMPT_DIALOG) {
        debugDump("gWindowObserver:observe:onLoad - load event for window " +
                  "not being tested - location: " + win.location +
                  "... returning early");
        return;
      }

      // The first wizard page should always be the dummy page.
      let pageid = win.document.documentElement.currentPage.pageid;
      if (pageid != PAGEID_DUMMY) {
        // This should never happen but if it does this will provide a clue
        // for diagnosing the cause.
        ok(false, "Unexpected load event - pageid got: " + pageid +
           ", expected: " + PAGEID_DUMMY + "... returning early");
        return;
      }

      gTimeoutTimer = AUS_Cc["@mozilla.org/timer;1"].
                      createInstance(AUS_Ci.nsITimer);
      gTimeoutTimer.initWithCallback(finishTestTimeout, TEST_TIMEOUT,
                                     AUS_Ci.nsITimer.TYPE_ONE_SHOT);

      gWin = win;
      gDocElem = gWin.document.documentElement;
      gDocElem.addEventListener("pageshow", onPageShowDefault, false);
    }, false);
  }
};
